feat(rag): offline re-index & retrieval hardening (config-gated, behavior-neutral)#213
Conversation
…ults unchanged) Lands the offline re-index / retrieval-improvement workstream. Every behavioural change ships default-off so the merge is behaviour-neutral; defaults reproduce the exact prior behavior and only flip after eval gates. - W6 ranking-config: extract the second-stage rerank weights, document-diversity demotion, and freshness decay into src/lib/ranking-config.ts with an optional RAG_RANKING_CONFIG JSON override. Defaults equal the prior constants; diversity penalty defaults to 0 (OFF). - CI-1 chunker: CHUNK_STRATEGY=page|document. "page" (default) is byte-for-byte the current page-bounded chunker; "document" is structure-aware cross-page chunking for the eval-gated shadow re-index only. - CI-8/9 embedding integrity: embedding-dimensions guard + embed-texts integrity checks; OPENAI_EMBEDDING_BATCH_SIZE (IDX-C3) splits a full-corpus re-embed into <=2048-input batches so it can't exceed the OpenAI request ceiling. - CI-4 stable chunk_key, CI-14 synonym recall (clinical-vocabulary), W3/CI-6 OCR-quality signals (index-quality), plus check-indexing coverage. - #6 reindex-eval-gate: pure, unit-tested GO/NO-GO non-degradation gate for shadow re-index cutover (absolute bars + no-regression vs live baseline); design doc in docs/reindex-shadow-harness-design.md. Test coverage added across chunking, embedding-dimensions, embed-texts integrity, index-quality, ranking-config, reindex-eval-gate, retrieval-query-variants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e; defer WCC golden case Follow-up to the offline re-index landing. Recognizes "white cell count"/WCC as a clozapine haematological-monitoring term so a WCC-phrased withhold-threshold query is routed to the structured clozapine_blood_action_structured_threshold gate and pulls the memory-card / table-fact evidence layer (previously it fell through to the generic threshold gate with no memory cards). Additive synonym only; tightly scoped to clozapine + withhold queries. Full golden suite shows no regression (ANC/CBC and all base cases unchanged). The withhold-action chunk still does not rank into top-5 for the WCC phrasing (a ranking, not routing, gap that needs care on this safety-critical path), so the unvalidated clozapine-wcc-abbreviation-threshold golden case is removed for now and tracked as a follow-up. The passing clozapine-cbc-abbreviation-threshold case is kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Line 11 in b0922cb
When CHUNK_STRATEGY=document, emitChunk still writes this constant into metadata.chunker_version, so shadow re-index rows produced with cross-page boundaries are labelled 1.0.0-page. Any stale-index/cutover/eval logic that relies on this metadata cannot tell page-bounded chunks from document-mode chunks, even though their chunk boundaries and source spans differ; make the version strategy-specific or include the strategy in metadata.
Line 731 in b0922cb
In document mode, a multi-page section passes all sectionImages into every emitted chunk, while emitChunk can attach an image based on section-path/header matches rather than the chunk text. For a long section with a table on a later page, an earlier text-only chunk can be stored with that unrelated table's image_ids, causing retrieval results/citations to show visual evidence from pages that did not contribute to the chunk; pass only images from contributing pages or otherwise page-filter them here.
Lines 554 to 555 in b0922cb
For document-mode chunks spanning multiple pages, every sourceSpanForText call receives the full cross-page excerpt. The first page may match, but later contributing pages search for text that starts on an earlier page and usually end up with null offsets, so citations cannot anchor to those pages; derive a page-specific excerpt (or split the chunk text by contributing page) before creating each span.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Lines 554 to 555 in 4a58cda
When CHUNK_STRATEGY=document emits a chunk spanning multiple pages, this calls sourceSpanForText for each contributing page with the same full-chunk excerpt. Because sourceSpanForText searches the first part of that excerpt in the page text, only the first page of the chunk can match; later pages get null offsets and an excerpt that starts with text from a different page. This breaks source anchors/highlighting for evidence that came from a continuation page, so the span should be built from a page-local snippet before storing each entry.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Lines 554 to 556 in 95cbc0b
When CHUNK_STRATEGY=document creates a chunk spanning multiple pages, this maps every contributing page through sourceSpanForText with the same full-chunk excerpt. Because sourceSpanForText only searches from the beginning of that excerpt, later pages usually get null offsets and an excerpt copied from page 1, so source metadata/citations for evidence that came from page 2+ point at the wrong text. Pass a page-specific excerpt (or split the chunk text per contributing page) when building each span.
Line 10 in 95cbc0b
The new document-mode chunker still stamps every row with CHUNKER_VERSION = "1.0.0-page". In the shadow re-index path that runs with CHUNK_STRATEGY=document, staged cross-page chunks become indistinguishable from page-bounded chunks in metadata, defeating the stated purpose of using the version to identify which chunker produced a row and to invalidate stale chunks after boundary changes. Include the active strategy/params in the stamped version before using document mode for eval-gated re-indexes.
Line 731 in 95cbc0b
In document mode this passes every image from the whole section into emitChunk, even when attributeChunkToPages has narrowed the chunk to only one or two contributing pages. For long sections with multiple tables/images, the heading/path matching in emitChunk can attach image_ids from pages outside page_start/page_end, so source-image/table requests may treat an unrelated page's visual as evidence for the chunk; pass only images from the contributing pages.
Lines 709 to 710 in 95cbc0b
With CHUNK_STRATEGY=document, this document-wide fingerprint drops every later chunk whose normalized content matches an earlier one, regardless of page or section. Identical clinical rows or repeated instructions can appear under different headings/pages and need distinct source attribution; page mode preserved those duplicates, but document mode now silently removes them from the staged index. Include section/page context in the dedupe key or restrict this to proven boilerplate.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Line 547 in 63dc5c1
When CHUNK_STRATEGY=document is enabled for the shadow re-index, buildDocumentModeChunks still reaches this shared emitter and stamps chunker_version with the constant value 1.0.0-page, with no separate strategy marker in the metadata. That makes document-mode rows indistinguishable from the default page-mode rows, so any stale-row/re-index health check using the advertised chunker version cannot tell whether the candidate was actually produced by the cross-page chunker; include the active strategy in the version or metadata.
Line 194 in 63dc5c1
This template literal contains actual 0x00 bytes in the TypeScript source, which makes Git classify src/lib/chunking.ts as binary and hides textual diffs for the whole file in normal review/patch tooling. The hash can still use NUL separators without making the source binary by writing them as escaped \0 characters instead.
Line 727 in 63dc5c1
When document-mode chunking merges content from pages 1–2 and the answer comes from the later page, this stores only the first contributing page as the chunk's page_number. The citation path uses result.page_number for labels and does not read metadata.page_start/page_end, so users can be sent to page 1 for evidence that came from page 2; propagate a page range into citations or avoid collapsing cross-page chunks to a single representative page.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 160ce6c755
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const contentWords = new Set( | ||
| normalizeLookupText(content.replace(imageDataTagPattern, " ")).split(/\s+/).filter(Boolean), | ||
| ); |
There was a problem hiding this comment.
Keep image pages contributing in document mode
When document-mode chunking produces a chunk whose evidence comes from inline image/table tags (for example scanned or image-only pages), this strips all of that tag text before page attribution, so contentWords can be empty and attributeChunkToPages falls back to only the first page. The chunk still contains the later pages' [[IMAGE_DATA_START]] text, but contributingImages, image_ids, page_start/page_end, and source spans are then limited to page 1, which can make source-image/table evidence from later pages unavailable after the shadow re-index.
Useful? React with 👍 / 👎.
| case_count: number; | ||
| case_fingerprint?: string; | ||
| failed_case_count: number; | ||
| document_recall_at_5: number; |
There was a problem hiding this comment.
Align gate inputs with actual eval summaries
Fresh evidence in this revision is that scripts/eval-retrieval.ts still emits failed_cases as an array, while this pass-through gate now requires failed_case_count; similarly the quality governance rates consumed below come from retrieval.source_governance, not the RAG summary object. A driver following the stated “pass summaries through unchanged” contract will therefore return NO_GO for every run due to missing required metrics instead of evaluating the candidate, so either the eval scripts need to emit these fields or the gate needs an adapter that derives them from the current summaries.
Useful? React with 👍 / 👎.
…on + linear freshness) (#242)
Lands the offline re-index / retrieval-improvement workstream. All new behavior is config-gated and default-off, so merging is behavior-neutral; defaults reproduce the exact prior behavior and only flip after eval gates.
What's included
src/lib/ranking-config.ts): the second-stage rerank weights, document-diversity demotion, and freshness decay are now one config object with an optionalRAG_RANKING_CONFIGJSON override. Defaults equal the prior constants; diversity penalty defaults to0(OFF).CHUNK_STRATEGY=page|document):page(default) is byte-for-byte the current page-bounded chunker;documentis structure-aware cross-page chunking for the eval-gated shadow re-index only.OPENAI_EMBEDDING_BATCH_SIZE(IDX-C3) splits a full-corpus re-embed into <=2048-input batches.chunk_key, CI-14 synonym recall (clinical-vocabulary), W3/CI-6 OCR-quality signals (index-quality), pluscheck:indexingcoverage.src/lib/reindex-eval-gate.ts): pure, unit-tested GO/NO-GO non-degradation gate for shadow re-index cutover; design doc indocs/reindex-shadow-harness-design.md.Verification
Deferred (tracked follow-up)
The clozapine-wcc-abbreviation-threshold golden case is removed for now (the passing clozapine-cbc case is kept). The WCC routing fix works, but surfacing the withhold-action chunk into top-5 for the WCC phrasing needs ranking tuning on this safety-critical path - deferred rather than hacked.
🤖 Generated with Claude Code